package se.webbzon.boltzmann.editor.mvc;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import javax.swing.JDesktopPane;

import se.webbzon.boltzmann.editor.factory.propertyframe.PropertyFrameFactory;
import se.webbzon.boltzmann.editor.factory.worldobject.WorldObjectFactory;
import se.webbzon.boltzmann.editor.h3ds.H3DSReader;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldarea.WorldAreaListener;
import se.webbzon.oschi01.worldeditor.DeflatableWorldObject;
import se.webbzon.oschi01.worldeditor.WorldAreaBlueprint;
import se.webbzon.oschi01.worldeditor.WorldAreaBlueprintException;
import se.webbzon.oschi01.worldeditor.WorldAreaReader;
import se.webbzon.oschi01.worldeditor.WorldAreaReaderException;
import se.webbzon.oschi01.worldeditor.WorldAreaWriter;
import se.webbzon.oschi01.worldeditor.WorldObjectInflatorGroup;
import se.webbzon.oschi01.worldobject.WorldObject;

public class EditorModel {
	
	// The world object inflator group used in this model
	private final WorldObjectInflatorGroup inflatorGroup;
	
	// The desktop pane used in the editor
	private final JDesktopPane desktopPane;
	
	// The world area listener used in this model
	private final WorldAreaListener worldAreaListener;
	
	// A collection of all the world object factories in this model
	private final Collection<WorldObjectFactory> worldObjectFactories;
	
	// A map of all the property frame factories in this model
	private final Map<String,PropertyFrameFactory> propertyFrameFactories;
	
	// A collection of all the listeners to this model
	private final Collection<EditorListener> listeners;
	
	// The 3-dimensional grid used in the model
	private final Grid3D grid;
	
	// The current selected file
	private File file;
	
	// The current editable world area 
	private WorldArea world;
	
	// A collection of all selected instances
	private Collection<WorldObject> selectedInstances;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new editor mvc-model. **/
	public EditorModel(	WorldObjectInflatorGroup inflatorGroup, 
						JDesktopPane desktopPane) {
		this.inflatorGroup = inflatorGroup;
		this.desktopPane = desktopPane;
		worldAreaListener = new WorldAreaListenerImpl();
		worldObjectFactories = new HashSet<WorldObjectFactory>();
		propertyFrameFactories = new HashMap<String,PropertyFrameFactory>();
		listeners = new ArrayList<EditorListener>();
		grid = new Grid3D(16,16,16);
		selectedInstances = null;
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the inflator group associated with this model. **/
	public WorldObjectInflatorGroup getInflatorGroup() {
		return inflatorGroup;
	}
	
	/** Returns the desktop pane associated with this model. **/
	public JDesktopPane getDesktopPane() {
		return desktopPane;
	}
	
	/** Adds a listener to this model. **/
	public boolean addListener(EditorListener listener) {
		return listeners.add(listener);
	}
	
	/** Removes a listener from this model. **/
	public boolean removeListener(EditorListener listener) {
		return listeners.remove(listener);
	}
	
	/** Clears all the listeners from this model. **/
	public void clearListener() {
		listeners.clear();
	}
	
	/** Adds a world object factory to this model. **/
	public void addWorldObjectFactory(WorldObjectFactory factory) {
		worldObjectFactories.add(factory);
	}
	
	/** Returns a collection of all the factories in this model. **/
	public Collection<WorldObjectFactory> getWorldObjectFactories() {
		return Collections.unmodifiableCollection(worldObjectFactories);
	}
	
	/** Adds a property frame factory to this model. **/
	public void addPropertyFrameFactory(PropertyFrameFactory factory) {
		propertyFrameFactories.put(factory.getWorldObjectName(), factory);
	}
	
	/** Returns the property frame factory associated with the given name. **/
	public PropertyFrameFactory getPropertyFrameFactory(String worldObjectName) {
		return propertyFrameFactories.get(worldObjectName);
	}
	
	/** Returns the selected file for this model. **/
	public File getFile() {
		return file;
	}
	
	/** Discards the selected file from the model, making saveWorld() and
	 * loadWorld() useless.**/
	public void discardFile(Object source) {
		this.file = null;
		for (EditorListener listener : listeners.toArray(new EditorListener[0]))
			listener.onSelectFile(source,null);
	}
	
	/** Saves the current editable world to the given file. If the world was
	 * successfully saved then the given file will be the selected file for this
	 * model. **/
	public void saveWorldAs(Object source, File file) throws IOException, WorldAreaBlueprintException {
		File tempFile = this.file;
		this.file = file;
		try {
			saveWorld();
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onSelectFile(source,file);
		} catch (IOException e) {
			this.file = tempFile;
			throw e;
		}
	}
	
	/** Saves the current editable world to the selected file. **/
	public void saveWorld() throws IOException, WorldAreaBlueprintException {
		if (file == null)
			throw new IOException("No file is selected in model.");
		if (world == null)
			throw new IOException("No world is open in the model.");
		
		Collection<WorldObject> instances = world.findInstances(WorldObject.class);
		ArrayList<DeflatableWorldObject> serializableInstances =
				new ArrayList<DeflatableWorldObject>();
		for (WorldObject instance : instances)
			if (instance instanceof DeflatableWorldObject)
				serializableInstances.add((DeflatableWorldObject) instance);
				
		WorldAreaBlueprint blueprint = new WorldAreaBlueprint(	"boltzmann",
																0,
																serializableInstances);
		
		if (getExtension(file).equals("3ds")) {
			throw new IOException(".3ds-files can not be overwritten!");
		} else {
			OutputStream outputStream = new FileOutputStream(file);
			WorldAreaWriter writer = new WorldAreaWriter();
			writer.write(outputStream, blueprint);
			outputStream.close();
		}
	}
	
	/** Loads the given file to the editor, making it the current editable world. 
	 * If the loading was successful then the given file will be the selected file
	 * for this model. **/
	public void loadWorldFrom(Object source, File file, ArrayList<WorldAreaBlueprintException> nonVitalExceptions) 
			throws IOException {
		File tempFile = this.file;
		this.file = file;
		try {
			loadWorld(source,nonVitalExceptions);
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onSelectFile(source,file);
		} catch (IOException e) {
			this.file = tempFile;
			throw e;
		}
	}
	
	/** Loads the selected file to the editor, making it the current editable world. **/
	public void loadWorld(Object source, ArrayList<WorldAreaBlueprintException> nonVitalExceptions) 
			throws IOException {
		if (file == null)
			throw new IOException("No file is selected in model.");
		
		InputStream inputStream = new FileInputStream(file);
		WorldArea world;
		try {
			if (getExtension(file).equals("3ds")) {
				H3DSReader reader = new H3DSReader();
				world = reader.load(inputStream);
				inputStream.close();
			} else {
				WorldAreaReader reader = new WorldAreaReader();
				WorldAreaBlueprint blueprint = reader.load(inputStream);
				world = blueprint.load(inflatorGroup,nonVitalExceptions);
			}
		} catch (WorldAreaReaderException | WorldAreaBlueprintException e) {
			throw new IOException(e);
		} finally {
			inputStream.close();
		}
		openWorld(source,world);
		
	}
	
	/** Returns true if the model already has an open world. **/
	public boolean hasOpenWorld() {
		return (world != null);
	}
	
	/** Returns the current open world. If no world is open 
	 * then null is returned. **/
	public WorldArea getWorld() {
		return world;
	}
	
	/** Opens a new world area for this model. **/
	public void openWorld(Object source, WorldArea world) {
		if (world != null) {
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onCloseWorld(source,world);
			selectedInstances = null;
			world.removeListener(worldAreaListener);
		}
		
		this.world = world;
		
		for (EditorListener listener : listeners.toArray(new EditorListener[0]))
			listener.onOpenWorld(source,world);
		world.addListener(worldAreaListener);
	}
	
	/** Closes the current editable world area for this model. This method
	 * will discard the currently selected file in the model. **/
	public void closeWorld(Object source) {		
		for (EditorListener listener : listeners.toArray(new EditorListener[0]))
			listener.onCloseWorld(source,world);
		
		world.removeListener(worldAreaListener);
		
		selectedInstances = null;
		discardFile(source);
		world = null;
	}
	
	/** Adds an instance to this model. **/
	public boolean addInstance(Object source, WorldObject instance) {
		if (world == null)
			return false;
		
		world.createInstance(instance);
		
		return true;
	}
	
	/** Removes an instance from this model. **/
	public boolean removeInstance(Object source, WorldObject instance) {
		if (world == null)
			return false;
		
		world.destroyInstance(instance);
		
		return true;
	}
	
	/** Notifies to the model that an instance in the model has been changed. **/
	public void changeInstance(Object source, WorldObject instance) {
		if (instance != null)
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onChangeInstance(source,instance);
	}
	
	/** Notifies to the model that an instance has got the focus. **/
	public void focusOnInstance(Object source, WorldObject instance) {
		if (instance != null)
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onFocusInstance(source,instance);
	}
	
	/** Returns a collection of all currently selected instances in this model. **/
	public Collection<WorldObject> getSelectedInstances() {
		if (selectedInstances == null)
			return new ArrayList<WorldObject>();
		else
			return Collections.unmodifiableCollection(selectedInstances);
	}
	
	/** Sets the selected instances in this model. **/
	public boolean selectInstances(Object source, Collection<WorldObject> instances) {
		if (world == null)
			return false;
		
		Collection<WorldObject> deselectedInstances;
		if (selectedInstances == null) {
			deselectedInstances = new ArrayList<WorldObject>();
		} else {
			selectedInstances.removeAll(instances);
			deselectedInstances = selectedInstances;
		}
		selectedInstances = instances;
		for (EditorListener listener : listeners.toArray(new EditorListener[0]))
			listener.onSelectInstances(source,selectedInstances,deselectedInstances);
		
		return true;
	}
	
	/** Returns the grid used in this model. **/
	public Grid3D getGrid() {
		return grid;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Returns the lower case file extension of a file. If the file has 
	 * no extension then an empty string is returned. **/
	public static String getExtension(File f) {
	    String ext = "";
	    String filename = f.getName();
	    int i = filename.lastIndexOf('.');

	    if (i > 0 &&  i < filename.length() - 1)
	        ext = filename.substring(i+1).toLowerCase();
	  
	    return ext;
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** An implementation of a world area listener. **/
	private final class WorldAreaListenerImpl implements WorldAreaListener {

		@Override public void onInstanceAdd(WorldArea source, WorldObject instance) {
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onAddInstance(this,instance);
		}

		@Override public void onInstanceRemove(WorldArea source, WorldObject instance) {
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onRemoveInstance(this,instance);
		}
		
		@Override public void onInstancesClear(WorldArea source) {
			for (EditorListener listener : listeners.toArray(new EditorListener[0]))
				listener.onClearInstances(this);
		}

		@Override public void onGoto(WorldArea source, WorldArea destination) {}
		
	}

}
